package handler

import (
	"github.com/DiracLee/dires-go/app/cmdline"
	"github.com/DiracLee/dires-go/app/database"
	"github.com/DiracLee/dires-go/app/payload"
	"strconv"
)

func init() {
	RegisterCommand(cmdline.CmdLPush, handleLPush, writeFirstKey, undoLPush, -3)
	RegisterCommand(cmdline.CmdLPushX, handleLPushX, writeFirstKey, undoLPush, -3)
	RegisterCommand(cmdline.CmdRPush, handleRPush, writeFirstKey, undoRPush, -3)
	RegisterCommand(cmdline.CmdRPushX, handleRPushX, writeFirstKey, undoRPush, -3)
	RegisterCommand(cmdline.CmdLPop, handleLPop, writeFirstKey, undoLPop, 2)
	RegisterCommand(cmdline.CmdRPop, handleRPop, writeFirstKey, undoRPop, 2)
	RegisterCommand(cmdline.CmdRPopLPush, handleRPopLPush, prepareRPopLPush, undoRPopLPush, 3)
	RegisterCommand(cmdline.CmdLRem, handleLRem, writeFirstKey, rollbackFirstKey, 4)
	RegisterCommand(cmdline.CmdLLen, handleLLen, readFirstKey, nil, 2)
	RegisterCommand(cmdline.CmdLIndex, handleLIndex, readFirstKey, nil, 3)
	RegisterCommand(cmdline.CmdLSet, handleLSet, writeFirstKey, undoLSet, 4)
	RegisterCommand(cmdline.CmdLRange, handleLRange, readFirstKey, nil, 4)
}

func handleLPush(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	values := args[1:]

	list, _, errPayload := db.GetOrInitList(key)
	if errPayload != nil {
		return errPayload
	}

	for _, value := range values {
		list.Insert(0, value)
	}

	db.AddAOF(NamedCommand(cmdline.CmdLPush, args...))
	return payload.NewIntPayload(int64(list.Len()))
}

func undoLPush(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	key := string(args[0])
	count := len(args) - 1
	cmdLines := make([]cmdline.CmdLine, 0, count)
	for i := 0; i < count; i++ {
		cmdLines = append(cmdLines, StringsCommand(cmdline.CmdLPop, key))
	}
	return cmdLines
}

func handleLPushX(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	values := args[1:]

	list, errPayload := db.GetAsList(key)
	if errPayload != nil {
		return errPayload
	}
	if list == nil {
		return payload.NewIntPayload(0)
	}

	for _, value := range values {
		list.Insert(0, value)
	}
	db.AddAOF(NamedCommand(cmdline.CmdLPushX, args...))
	return payload.NewIntPayload(int64(list.Len()))
}

func handleRPush(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	values := args[1:]

	list, _, errPayload := db.GetOrInitList(key)
	if errPayload != nil {
		return errPayload
	}

	for _, value := range values {
		list.Append(value)
	}

	db.AddAOF(NamedCommand(cmdline.CmdRPush, args...))
	return payload.NewIntPayload(int64(list.Len()))
}

func undoRPush(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	key := string(args[0])
	count := len(args) - 1
	cmdLines := make([]cmdline.CmdLine, 0, count)
	for i := 0; i < count; i++ {
		cmdLines = append(cmdLines, StringsCommand(cmdline.CmdRPop, key))
	}
	return cmdLines
}

func handleRPushX(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	values := args[1:]

	list, errPayload := db.GetAsList(key)
	if errPayload != nil {
		return errPayload
	}
	if list == nil {
		return payload.NewIntPayload(0)
	}

	for _, value := range values {
		list.Append(value)
	}
	db.AddAOF(NamedCommand(cmdline.CmdRPushX, args...))
	return payload.NewIntPayload(int64(list.Len()))
}

func handleLPop(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])

	list, errPayload := db.GetAsList(key)
	if errPayload != nil {
		return errPayload
	}
	if list == nil {
		return &payload.NullBulkPayload{}
	}

	raw, _ := list.Remove(0)
	val, _ := raw.([]byte)
	if list.Len() == 0 {
		db.Delete(key)
	}
	db.AddAOF(NamedCommand(cmdline.CmdRPop, args...))
	return payload.NewBulkPayload(val)
}

func undoLPop(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	key := string(args[0])
	list, errPayload := db.GetAsList(key)
	if errPayload != nil {
		return nil
	}
	if list == nil || list.Len() == 0 {
		return nil
	}
	raw, _ := list.Get(0)
	element := raw.([]byte)
	return []cmdline.CmdLine{
		{
			[]byte(cmdline.CmdLPush),
			args[0],
			element,
		},
	}
}

func handleRPop(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])

	list, errPayload := db.GetAsList(key)
	if errPayload != nil {
		return errPayload
	}
	if list == nil {
		return &payload.NullBulkPayload{}
	}

	raw := list.RemoveLast()
	val, _ := raw.([]byte)
	if list.Len() == 0 {
		db.Delete(key)
	}
	db.AddAOF(NamedCommand(cmdline.CmdRPop, args...))
	return payload.NewBulkPayload(val)
}

func undoRPop(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	key := string(args[0])
	list, errPayload := db.GetAsList(key)
	if errPayload != nil {
		return nil
	}
	if list == nil || list.Len() == 0 {
		return nil
	}
	raw, _ := list.Get(list.Len() - 1)
	element, _ := raw.([]byte)
	return []cmdline.CmdLine{
		{
			[]byte(cmdline.CmdRPush),
			args[0],
			element,
		},
	}
}

func handleRPopLPush(db database.DB, args cmdline.CmdLine) payload.Payload {
	sourceKey := string(args[0])
	destKey := string(args[1])

	sourceList, errPayload := db.GetAsList(sourceKey)
	if errPayload != nil {
		return errPayload
	}
	if sourceList == nil {
		return &payload.NullBulkPayload{}
	}

	destList, _, errPayload := db.GetOrInitList(destKey)
	if errPayload != nil {
		return errPayload
	}

	val, _ := sourceList.RemoveLast().([]byte)
	destList.Insert(0, val)

	if sourceList.Len() == 0 {
		db.Delete(sourceKey)
	}

	db.AddAOF(NamedCommand("rpoplpush", args...))
	return payload.NewBulkPayload(val)
}

func undoRPopLPush(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	sourceKey := string(args[0])
	list, errPayload := db.GetAsList(sourceKey)
	if errPayload != nil {
		return nil
	}
	if list == nil || list.Len() == 0 {
		return nil
	}
	raw, _ := list.Get(list.Len() - 1)
	element, _ := raw.([]byte)
	return []cmdline.CmdLine{
		{
			[]byte(cmdline.CmdRPush),
			args[0],
			element,
		},
		{
			[]byte(cmdline.CmdLPop),
			args[1],
		},
	}
}

func handleLRem(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	count64, err := strconv.ParseInt(string(args[1]), 10, 64)
	if err != nil {
		return payload.NewErrPayload("ERR value is not an integer or out of range")
	}
	count := int(count64)
	value := args[2]

	list, errPayload := db.GetAsList(key)
	if errPayload != nil {
		return errPayload
	}
	if list == nil {
		return payload.NewIntPayload(0)
	}

	var removed int
	if count == 0 {
		removed = list.RemoveValued(value)
	} else if count > 0 {
		removed = list.RemoveValued(value, count)
	} else {
		removed = list.ReverseRemoveValued(value, -count)
	}

	if list.Len() == 0 {
		db.Delete(key)
	}
	if removed > 0 {
		db.AddAOF(NamedCommand("lrem", args...))
	}

	return payload.NewIntPayload(int64(removed))
}

func handleLLen(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])

	list, errPayload := db.GetAsList(key)
	if errPayload != nil {
		return errPayload
	}
	if list == nil {
		return payload.NewIntPayload(0)
	}

	size := int64(list.Len())
	return payload.NewIntPayload(size)
}

func handleLIndex(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	index64, err := strconv.ParseInt(string(args[1]), 10, 64)
	if err != nil {
		return payload.NewErrPayload("ERR value is not an integer or out of range")
	}
	index := int(index64)

	list, errPayload := db.GetAsList(key)
	if errPayload != nil {
		return errPayload
	}
	if list == nil {
		return &payload.NullBulkPayload{}
	}

	size := list.Len() // assert: size > 0
	if index < -1*size {
		return &payload.NullBulkPayload{}
	} else if index < 0 {
		index = size + index
	} else if index >= size {
		return &payload.NullBulkPayload{}
	}

	raw, _ := list.Get(index)
	val, _ := raw.([]byte)
	return payload.NewBulkPayload(val)
}

func handleLSet(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	index64, err := strconv.ParseInt(string(args[1]), 10, 64)
	if err != nil {
		return payload.NewErrPayload("ERR value is not an integer or out of range")
	}
	index := int(index64)
	value := args[2]

	list, errPayload := db.GetAsList(key)
	if errPayload != nil {
		return errPayload
	}
	if list == nil {
		return payload.NewErrPayload("ERR no such key")
	}

	size := list.Len() // assert: size > 0
	if index < -1*size {
		return payload.NewErrPayload("ERR Index out of range")
	} else if index < 0 {
		index = size + index
	} else if index >= size {
		return payload.NewErrPayload("ERR Index out of range")
	}

	list.Set(index, value)
	db.AddAOF(NamedCommand(cmdline.CmdLSet, args...))
	return &payload.OkPayload{}
}

func undoLSet(db database.DB, args cmdline.CmdLine) []cmdline.CmdLine {
	key := string(args[0])
	index64, err := strconv.ParseInt(string(args[1]), 10, 64)
	if err != nil {
		return nil
	}
	index := int(index64)
	list, errPayload := db.GetAsList(key)
	if errPayload != nil {
		return nil
	}
	if list == nil {
		return nil
	}
	size := list.Len() // assert: size > 0
	if index < -1*size {
		return nil
	} else if index < 0 {
		index = size + index
	} else if index >= size {
		return nil
	}
	raw, _ := list.Get(index)
	value, _ := raw.([]byte)
	return []cmdline.CmdLine{
		{
			[]byte(cmdline.CmdLSet),
			args[0],
			args[1],
			value,
		},
	}
}

func handleLRange(db database.DB, args cmdline.CmdLine) payload.Payload {
	key := string(args[0])
	start64, err := strconv.ParseInt(string(args[1]), 10, 64)
	if err != nil {
		return payload.NewErrPayload("ERR value is not an integer or out of range")
	}
	start := int(start64)
	stop64, err := strconv.ParseInt(string(args[2]), 10, 64)
	if err != nil {
		return payload.NewErrPayload("ERR value is not an integer or out of range")
	}
	stop := int(stop64)

	list, errPayload := db.GetAsList(key)
	if errPayload != nil {
		return errPayload
	}
	if list == nil {
		return &payload.EmptyMultiBulkPayload{}
	}

	size := list.Len() // assert: size > 0
	if start < -1*size {
		start = 0
	} else if start < 0 {
		start = size + start
	} else if start >= size {
		return &payload.EmptyMultiBulkPayload{}
	}
	if stop < -1*size {
		stop = 0
	} else if stop < 0 {
		stop = size + stop + 1
	} else if stop < size {
		stop = stop + 1
	} else {
		stop = size
	}
	if stop < start {
		stop = start
	}

	slice := list.Range(start, stop)
	result := make(cmdline.CmdLine, len(slice))
	for i, raw := range slice {
		bytes, _ := raw.([]byte)
		result[i] = bytes
	}
	return payload.NewMultiBulkPayload(result)
}
